package restdom.validators;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;

import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import restdom.dao.GenericDao;
import restdom.dao.GenericIdResolver;
import restdom.dao.impl.ReflectiveIdResolver;

public class GenericDaoValidator<T, ID extends Serializable> implements
		Validator {

	private GenericDao<T, ID> dao;

	private Class<T> entityClass;

	private String propertyName;

	private GenericIdResolver<T, ID> idResolver;

	public GenericDaoValidator(GenericDao<T, ID> dao, Class<T> entityClass,
			GenericIdResolver<T, ID> idResolver) {
		this(dao, entityClass, idResolver, null);
	}

	public GenericDaoValidator(GenericDao<T, ID> dao, Class<T> entityClass,
			Class<ID> idClass) {
		this(dao, entityClass, new ReflectiveIdResolver<T, ID>(entityClass,
				idClass), null);
	}

	public GenericDaoValidator(GenericDao<T, ID> dao, Class<T> entityClass,
			Class<ID> idClass, String idPropertyName) {
		this(dao, entityClass, new ReflectiveIdResolver<T, ID>(entityClass,
				idClass, idPropertyName), null);
	}

	public GenericDaoValidator(GenericDao<T, ID> dao, Class<T> entityClass,
			Class<ID> idClass, String idPropertyName, String propertyName) {
		this(dao, entityClass, new ReflectiveIdResolver<T, ID>(entityClass,
				idClass, idPropertyName), propertyName);
	}

	public GenericDaoValidator(GenericDao<T, ID> dao, Class<T> entityClass,
			GenericIdResolver<T, ID> idResolver, String propertyName) {
		if (dao == null) {
			throw new IllegalArgumentException("Generic DAO must be specified");
		}
		this.dao = dao;
		if (entityClass == null) {
			throw new IllegalArgumentException("Entity class must be specified");
		}
		this.entityClass = entityClass;
		if (idResolver == null) {
			throw new IllegalArgumentException(
					"Identifier resolver must be specified");
		}
		this.idResolver = idResolver;
		this.propertyName = propertyName;
	}

	public boolean supports(Class clazz) {
		if (StringUtils.hasText(propertyName)) {
			try {
				return entityClass.isAssignableFrom(getReturnType(clazz
						.getMethod(getPropertyGetter(propertyName))));
			} catch (SecurityException e) {
				// Returns false
			} catch (NoSuchMethodException e) {
				// Returns false
			}
		} else {
			return entityClass.isAssignableFrom(clazz)
					|| hasEntityProperties(clazz);
		}
		return false;
	}

	protected Class<?> getReturnType(Method method) {
		if (Collection.class.isAssignableFrom(method.getReturnType())) {
			return GenericCollectionTypeResolver
					.getCollectionReturnType(method);
		}
		return method.getReturnType();
	}

	protected boolean hasEntityProperties(Class clazz) {
		for (Method method : clazz.getMethods()) {
			if (isGetterMethod(method)) {
				Class returnType = getReturnType(method);
				if (entityClass.isAssignableFrom(returnType)) {
					return true;
				}
			}
		}
		return false;
	}

	public void validate(Object target, Errors errors) {
		if (StringUtils.hasText(propertyName)) {
			validateProperty(target, errors, propertyName);
		} else if (hasEntityProperties(target.getClass())) {
			validateProperties(target, errors);
		} else {
			validateObject(target, errors);
		}
	}

	private void validateObject(Object target, Errors errors) {
		@SuppressWarnings("unchecked")
		ID id = idResolver.getIdValue((T) target);
		try {
			T persistentObject = dao.read(id);
			if (persistentObject == null || !persistentObject.equals(target)) {
				reject(errors, id);
			}
		} catch (ObjectRetrievalFailureException e) {
			reject(errors, id);
		}
	}

	private void reject(Errors errors, Object... params) {
		errors.rejectValue(null, getErrorCode(), params, getErrorCode());
	}

	private String getErrorCode() {
		return "GenericDaoValidator[invalid]";
	}

	private void validateProperty(Object target, Errors errors, Method getter) {
		String propertyName = Character.toLowerCase(getter.getName().charAt(3))
				+ getter.getName().substring(4);
		validateProperty(target, errors, propertyName);
	}

	private void validateProperty(Object target, Errors errors,
			String propertyName) {
		errors.pushNestedPath(propertyName);
		try {
			Method propertyGetter = target.getClass().getMethod(
					getPropertyGetter(propertyName));
			Object propertyValue = propertyGetter.invoke(target);
			if (Collection.class.isAssignableFrom(propertyGetter
					.getReturnType())) {
				for (Object element : (Collection) propertyValue) {
					validateObject(element, errors);
				}
			} else {
				validateObject(propertyValue, errors);
			}
		} catch (IllegalArgumentException e) {
			reject(errors);
		} catch (SecurityException e) {
			reject(errors);
		} catch (IllegalAccessException e) {
			reject(errors);
		} catch (InvocationTargetException e) {
			reject(errors);
		} catch (NoSuchMethodException e) {
			reject(errors);
		}
		errors.popNestedPath();
	}

	private void validateProperties(Object target, Errors errors) {
		for (Method method : target.getClass().getMethods()) {
			if (isGetterMethod(method)) {
				Class returnType = getReturnType(method);
				if (entityClass.isAssignableFrom(returnType)) {
					validateProperty(target, errors, method);
				}
			}
		}
	}

	private boolean isGetterMethod(Method method) {
		String methodName = method.getName();
		return methodName.startsWith("get")
				&& methodName.charAt(3) == Character.toUpperCase(methodName
						.charAt(3));
	}

	protected String getPropertyGetter(String property) {
		return "get" + Character.toUpperCase(property.charAt(0))
				+ property.substring(1);
	}

}
